
/**
 * Auteur : Dominique GUERIN
 * dominiquephDOTguerinATgmailDOT..
 * dominiqueDOTguerinATinseeDOTfr(ance)  
 * Remerciements: � Keith Brown pour ses deux livres sur la s�curit� sous Windows et ses diff�rents articles
 *                 et � Michel Barnett de Microsoft pour ses deux articles 
 *                ".NET Remoting Authentication and Authorization Sample"  et le code qui les accompagne
 *  Le code est utilisable, modifiable et redistribuable � volont� sous la condition de ne pas supprimer ces 7 lignes.
 */ 
 
 package fr.doume.jna.authenticator;


import java.io.IOException;
import java.security.Principal;
import java.util.Date;

import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.deploy.LoginConfig;


//tomcat 5.5
//import org.apache.commons.logging.Log;
//import org.apache.commons.logging.LogFactory;

//tomcat 6
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;

import org.apache.catalina.authenticator.AuthenticatorBase;

import fr.doume.jna.sspi.SSPException;
import fr.doume.jna.sspi.SSPAuthentication;
import fr.doume.jna.sspi.SSPAuthShadow;
import fr.doume.jna.sspi.TranslationRolesIntoSids;
import fr.doume.jna.sspi.WindowsContext;

import fr.doume.jna.realm.WindowsRealm;
import fr.doume.jna.authenticator.Parameters;
import fr.doume.jna.authenticator.AuthenticationContext;
import fr.doume.jna.authenticator.HashMapWithTimeOut;

import org.apache.catalina.authenticator.Constants;

import org.apache.catalina.Context;
import org.apache.catalina.core.StandardContext;
import javax.servlet.ServletContext;

import java.util.ArrayList;
//import java.util.concurrent.ConcurrentHashMap;

import org.apache.catalina.realm.GenericPrincipal;

import fr.doume.jna.base64.Base64;
import fr.doume.jna.base64.FormatBase64Exception;

import org.apache.catalina.Realm;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.ServletException;

/**
 * The class <c><b>SSPAuthenticator </b></c> implements the authentication defined 
 * in the RFC 4559: "SPNEGO-based Kerberos and NTLM HTTP Authentication in 
 * Microsoft Windows".<p> This authentication is only for intranet<br/>
 * SSPI=Security Service Provider Interface<br>
 * GSSAPI=Generic Security Services Application Programming Interface <p>
 * The SPNEGO authentication uses GSSAPI or SSPI.
 * "The GSSAPI, by itself, does not provide any security. 
 * Instead, security service vendors provide GSSAPI implementations usually in the form of libraries 
 * installed with their security software. SSPI is  the version of Microsoft of GSSAPI.
 * The definitive feature of GSSAPI applications is the exchange of opaque messages (tokens) 
 * that hide the implementation detail from the higher level application. 
 * The client and server sides of the application are written to convey the 
 * tokens given to them by their respective GSSAPI implementations." 
 * "GSSAPI tokens can usually be sent over an insecure network as the mechanisms provide inherent message security. 
 * After some number of tokens have been exchanged, the GSSAPI implementations at both ends inform 
 * their local application that a security context has been established."
 * On HTTP, the tokens are sent in the headers Authorization and WWW-Authenticate.
 * The content of the header Authorization is given to the method AccepSecContext. This method returns a token sent
 * in the header WWW-Authenticate. Before to call InitSecContext or AcceptSecContex, the client and the server call AcquireCred
 * In this version, the class<code>SSPAuthentication</code> call the SSPI API via JNA.<br>
 *
 * If more than one exchange between the browser and the server is required (NTLM), 
 * we add an instance of the class <c>SSPAuthentication</c> in a session.
 * With Kerberos there is only one exchange.
 * To translate the names of the tomcat roles, a java class <code>TranslationRolesIntoSids</code> call 
 * <c>LookupAccountName</c> via JNA.
 * It translates the names into Sids with twos lines of code:
 *
 * The possibility has been added to not use SPNEGO, but directly NTLM, if we set the parameter <code>onlyntlm</code>.
 * This parameter is not recommended, but with some clients...
 *
 *
 * <br/><b>Configuration:</b><br/>
 * <ol> <b>Tomcat 5.5</b>: 
 * <li>The jar file ((frdoumesspitc5v3.jar) must be in the directory <code>/server/lib</code> of tomcat.
 * <li>The file <code>webapps/<application>/META-INF/context.xml</code> of the application must contain:
 * <pre>
 *    &lt;Valve className="fr.doume.jna.authenticator.SSPAuthenticator" />
 *    &lt;Realm className="fr.doume.jna.realm.WindowsRealm" />
 *
 *     &lt;!-- By default, not used. This parameter is not recommended
 *     &lt;Parameter name="onlyntlm" value="" override="false"/>
 *     -->
 *     &lt;!-- Time life in ms of the translation of the names of roles into Sids: By Default without limit --> 
 *     &lt;!--
 *     &lt;Parameter name="timelifemaprolesintosids" value="-1" override="false" />
 *     -->
 *     -->
 *     &lt;!-- By default, not used
 *     &lt;Parameter name="nogroupsinad" value="" override="false" />
 *     -->
 * 
 * </pre>
 * <li> The web.xml must contain information on the protected zone(s) of the application: For instance:
 * <pre>
 *   &lt;security-constraint>
 *     &lt;display-name>Managers Security Constraint</display-name>
 *     &lt;web-resource-collection>
 *        &lt;web-resource-name>Protected Area Gestionnaires&lt;/web-resource-name>
 *         &lt;!-- Define the context-relative URL(s) to be protected -->
 *        &lt;url-pattern>/manage/*&lt;/url-pattern>
 *      &lt;/web-resource-collection>
 *     &lt;auth-constraint>
 *        &lt;!-- Anyone with one of the listed roles may access this area -->
 *        &lt;role-name>DOMAINADNAME\Gestionnaires&lt;/role-name>
 *        &lt;role-name>DOMAINADNAME\pipo&lt;/role-name>
 *     &lt;/auth-constraint>
 *   &lt;/security-constraint>
 *   &lt;security-constraint>
 *     &lt;display-name>Staff Security Constraint&lt;/display-name>
 *     &lt;web-resource-collection>
 *        &lt;web-resource-name>Staff Protected Area&lt;/web-resource-name>
 *         &lt;!-- Define the context-relative URL(s) to be protected -->
 *        &lt;url-pattern>/staff/*&lt;/url-pattern>
 *      &lt;/web-resource-collection>
 *     &lt;auth-constraint>
 *        &lt;!-- Anyone with one of the listed roles may access this area -->
 *        &lt;role-name>DOMAINADNAME\Chefs&lt;/role-name>
 *     &lt;/auth-constraint>
 *   &lt;/security-constraint>
 *   &lt;security-constraint>
 *     &lt;display-name>Authenticated Users&lt;/display-name>
 *     &lt;web-resource-collection>
 *        &lt;web-resource-name>Authenticated Protected Area&lt;/web-resource-name>
 *         &lt;!-- Define the context-relative URL(s) to be protected -->
 *        &lt;url-pattern>/app/*&lt;/url-pattern>
 *      &lt;/web-resource-collection>
 *     &lt;auth-constraint>
 *        &lt;!-- Authenticated users may access this area -->
 *        &lt;role-name>*&lt;/role-name>
 *     &lt;/auth-constraint>
 *   &lt;/security-constraint>
 *
 *   &lt;login-config>
 *     &lt;auth-method>BASIC&lt;/auth-method>
 *     &lt;realm-name>Example Windows Authentication Area&lt;/realm-name>
 *   &lt;/login-config>
 *       
 *   &lt;!-- Security roles referenced by this web application -->
 *   &lt;security-role>
 *     &lt;role-name>users&lt;/role-name>
 *   &lt;/security-role>
 *   &lt;security-role>
 *     &lt;role-name>DOMAINADNAME\Domain users&lt;/role-name>
 *   &lt;/security-role>    
 *   &lt;security-role>
 *     &lt;role-name>tomcat&lt;/role-name>
 *   &lt;/security-role>
 *   &lt;security-role>
 *     &lt;role-name>DOMAINADNAME\Gestionnaires&lt;/role-name>
 *   &lt;/security-role>  
 *   &lt;security-role>
 *     &lt;role-name>DOMAINADNAME\Chefs&lt;/role-name>
 *   &lt;/security-role>  
 *   &lt;security-role>
 *     &lt;role-name>DOMAINADNAME\pipo&lt;/role-name>
 *   &lt;/security-role>
 * </pre>
 *
 *  Remember, the order of the elements in a file <code>web.xml</code> is:
 * <pre>
 *           icon
 *           display-name
 *           description
 *           distributable
 *           context-param
 *           filter
 *           filter-mapping
 *           listener
 *           servlet
 *           servlet-mapping
 *           session-config
 *           mime-mapping
 *           welcome-file-list
 *           error-page
 *           taglib
 *           resource-env-ref
 *           resource-ref
 *           security-constraint
 *           login-config
 *           security-role
 *           env-entry
 *           ejb-ref
 *           ejb-local-ref
 * </pre>
 * </ol>
 * <ol> <b>Tomcat 6</b>: 
 * <li>The jar file must be in the directory <code>/server/lib</code> of Tomcat 6.
 * <li>The content to add in the files web.xml and context.xml for Tomcat 6 are the same than for Tomcat 5.5.
 * <li> <ol> With tomcat 5.5 the logs use the java class given by commons logging. We find
 *           these classes in <code>common/lib/commons-looging-api.jar</code>.
 *           These classes are not given by default for Tomcat 6
 *           There are two solutions:
 *     <li> Replace the  two imports with <code>org.apache.commons.logging</code> by <code>org.apache.juli.logging</code>
 *          and complie the java code.(frdoumesspitc6v3.jar)
 *     <li> Use the jar given for tomcat 5.5 (frdoumesspitc5v3.jar) and add commons-logging-api.jar 
 *          in the directory <code>/bin</code> of Tomcat 6. 
 *          You can download this jar file from <code>http://commons.apache.org</code> 
 *     </ol> 
 * </ol>
 */
public class SSPAuthenticator
    extends AuthenticatorBase {
    private static Log log = LogFactory.getLog(SSPAuthenticator.class);

  // ----------------------------------------------------- Static Variables
 /**
   * Key Session attribute 
   *
   */
  private final static String SSPIAUTHENTICATION_KEY = 
    "fr.doume.jna.spnego.SSPIAuthentication";
 
 /**
   * Key Context attribute 
   * "fr.doume.jna.authenticator.AuthenticationContext".
   */
  private final static String CONTEXT_KEY =
    "fr.doume.jna.authenticator.AuthenticationContext";
    
  private final static String ALREADYCALLED_KEY =
    "fr.doume.jna.authenticator.NewCall";
  
  private final static String LOGIN_KEY =
    "fr.doume.jna.authenticator.Login";

    /**
     * Descriptive information about this implementation.
     */
  protected static final String info =
      "fr.doume.jna.authenticator.SSPAuthenticator/1.0";


    // --------------------------------------------------------- Public Methods

    /**
     * Return descriptive information about this Valve implementation.
     */
  public String getInfo() {

      return (info);

  }
    
    /**
     * This method is called when the client do a request with an uri wich is in a protected zone.
     * Returns true when the client is authenticated, else false with the stus code 401.
     * The first time, this method is called for an apllication, authenticate cerate an AuthenticationContext,
     * reads the parameters defined in the file context.xml and create a singleton Parameters, and try to 
     * translate the names of the roles tomcat defined in the file web.xm via the class TranslateNamesIntoSids which
     * sends the names to the Windows service. Negoserver responds with the Sids.
     * Then authenticate takes the header Authorization, decodes it from Base64, and sends it to the windows service wich use the dotnet 
     * class NegotiateStream. Negoserver sens also the name of the client, and the Sids translated from the names of roles 
     * that are in the access token.
     * To configure Firefox or IE, see the file <code>configure.txt</code>
     * The browser (Firefox or IE) try to use Kerberos, only when the uri uses a name that can be found in Active Directory.
     * On Windows, Negosesrver must be running on the same server as tomcat. Negosesrver uses the account SYSTEM or Network Service.
     * A server (or worksation) has an account with a Service Principal Name by default (HOST/<nameoftheserver>). The browser search 
     * a Service Principal Name HTTP/<nameoftheserver>. The KDC search a Service Principal Name HTTP(or HOST)/<nameoftheserver>.
     * If you want to use another name, you can define a CNAME in your DNS. The browser will use the translation (entry A in your DNS) 
     * to search the Srevice Principal Name.
     * If Tomcat is running on Unix, You must define an entry in your DNS and add HTTP/<this entry DNS> as Service Principal Name
     * to the account uses to run NegoServer. If Negoserver is running with SYSTEM or Network Service, you add this Service Principal
     * Name to the server account where negoserver is running.
     * For instance, if SYSTEM or Network Service is the account of Negoserver on the server windowssrv.test.net and if
     * Tomcat bis running on unixsrv.test.net, I can use the command:
     *  <pre>
     *            <code> setspn -A HOST/unixsrv.test.net TEST\windowssrv </code>  
     *                 or
     *            <code> setspn -A HTTP/unixsrv.test.net TEST\windowssrv </code>    
     *  </pre> 

     * If there is no SPN HTTP/nameoftheserver found in Active Directory, Fierfox or IE will use NTLM.
     * For more information, see <code>configure.txt</code>
     *   
     *  
     * @param request Requset to process
     * @param response Response to send
     * @param config   Not use in this Authenticator
     *
     * @exception IOException si une erreur d'E/S a lieu.
     */
    public boolean authenticate(Request request,
                                HttpServletResponse response,
                                LoginConfig config)
      throws IOException
          //par sendError
    {
    
      //setSecurePagesWithPragma(false);
      //boolean firsrtRoundTrip = true;

        // Is someone already authenticated?
      Principal principal = request.getUserPrincipal();
        
      if (principal != null) {
        if (log.isDebugEnabled())  log.debug("Already authenticated '" + principal.getName() + "'");
        
        HttpSession session = request.getSession(false);
        if (session != null)
        {
          if (session.getAttribute(LOGIN_KEY) != null)
          {
            session.removeAttribute(LOGIN_KEY);
            return true;
          }
        }
        
        // If the header Authorization is present 
        //  and if it is not the firs calll during the request processing, mustAuthenticate returns true 
        if ( ! mustAuthenticate(request) )
        {
          return true;
        }
        if (log.isDebugEnabled())  log.debug("Header Authorization given by the navigator: we must authenticate the client");
      }
           
      if (log.isDebugEnabled())
      {
        log.debug("Entry in  authenticate. Principal does not exist or the browser send the header Authorization: authentication is required"); 
      }
 
      Realm realm = context.getRealm();
      if( realm == null ){
        log.warn("No  Realm is configured or defined");
        response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE );
        return false;
      }
      if (log.isDebugEnabled()) log.debug("realm : " + realm);    
      
      TranslationRolesIntoSids trd = null;
      AuthenticationContext ac = null;
      
      Context ctx = request.getContext();
      ac  = getAuthenticationContext(ctx);
      if (ac == null){
        response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE );
        return false;
      }

      Parameters p = ac.getParameters();
      
      if (p.areGroupsInAd())
      {
        trd  = getTranslationRolesIntoSids(ctx, ac);
        if (trd == null){
          response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE );
          return false;
        }
      }

      if (request.getHeader("Authorization") == null)
      {
        setHeadersWWWAuthenticate(p, response);
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
        return false;
      }

       // The tomcat roles are translated into Windows SIDs( security identifiers)
      String clientName = null;
      log.debug("before getSSPAuthentication")	;
      
      if ( p.isOnlyKerberos() )     
      {
        if (isNtlmAuthentication(p, request))
        {
          if (log.isDebugEnabled()) log.debug("Ntlm Authentication");
          //response.setHeader("WWW-Authenticate", p.NEGOTIATE);
          response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
          return false;
        }
      }
      
      SSPAuthentication sspauth = getSSPAuthentication(request, ac);

      try{
        getHeaderAuthorizationAndSetWWW_Authenticate(request,response,sspauth, p);
      }
      catch(FormatBase64Exception e)
      // The token sent by the client is not in the format Base64
      {
        RemoveSSPAuthenticationFromContainer(request, ac);
        if (log.isDebugEnabled()) log.debug(e.getClass().getName() + " : " + e.getMessage());       
        if (log.isDebugEnabled()) log.debug(request.getRemoteAddr() + " Resources hContext and hCredental disposed : " + sspauth.isDisposed() );       
        response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
        return false;        
      }
      
      
      if (sspauth.isEstablished())
      {
        RemoveSSPAuthenticationFromContainer(request, ac);

        if (log.isDebugEnabled()) log.debug(request.getRemoteAddr() + " Authentication is established. Are hContext and hCredental disposed : " + sspauth.isDisposed());

        //try/catch required by java. 
        // Exception is not possible cf( the precondition isEstablished() in SSPAuthentication)
        try{         
          clientName = sspauth.getUserName();
        }
        catch (SSPException e) { log.fatal("This Exception could not exist: " + e.getMessage()); }
      }
      else if (sspauth.isFailed())
      {
        //error: remove SSPAuthentication from the session
        RemoveSSPAuthenticationFromContainer(request, ac);

        if (log.isDebugEnabled()) log.debug(request.getRemoteAddr() + " Authentication is failed. Are hContext and hCredental disposed : " + sspauth.isDisposed());
   
        if (p.canUserChooseAccount())
        { 
          setHeadersWWWAuthenticate(p, response);
        }
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
        return false;
      }
      else
      { //another exchange bettween the server and the client is requuired => send the status code 401
        //SSPAuthentication is not removed from the session
        if (log.isDebugEnabled()) 
            log.debug("Authentication is neither failed nor established: Another round between the client and the server is required");

        //setSSPAuthenticationInSession( request, sspauth);
        setSSPAuthenticationInContainer( request, sspauth, ac);
        
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
        return false;
      } 
        
        //sspauth.isEstablished
      if (clientName != null)
      { 
        if (p.isUserNameWithoutDomainName())
        {
          //SSPI sends as name like DomaiName\ClientName
          //We want to have only ClientName
          int position = clientName.indexOf('\\');
          String clientTemp = clientName;
          if (clientTemp.length() > position){
            clientTemp = clientTemp.substring(position+1);
          }
          if (log.isDebugEnabled()){
            if (log.isDebugEnabled()) log.debug("clientName :" + clientTemp);
          }
          clientName = clientTemp;
        }

        principal = getPrincipal(p, clientName, trd, sspauth);

        if (principal != null)
        {
        //Crete a session?
          HttpSession session = request.getSession(p.mustCreateSession());
          //register the principal
          register(request, response, principal,
                 "DIGEST",
                 clientName, clientName);
          if (log.isDebugEnabled()) log.debug("the client is now registered");
          request.setAttribute(ALREADYCALLED_KEY, "1");         
          return (true);
        }
        else
        {
          //We cannot be here, if a realm is defined
          if (log.isDebugEnabled())
              log.debug("User authenticated but we cannot create a new GenericPrincipal.");
              response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
          return false;
        }
      } //if (clientName != null)
      if (log.isDebugEnabled()) log.debug("clientName == null && authenticated!!! curious..."); 
                response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
      return (false); 
    }

    // --------------------------------------------------------- Private Methods
    private Principal getPrincipal(Parameters p, String clientName, TranslationRolesIntoSids trd, SSPAuthentication sspauth)
    {

        Principal  principal = null;
        ArrayList<String> roles = null;
        
        Realm realm;
        try
        {
          realm = context.getRealm();
        }
        catch(Exception e)
        {
          if (log.isDebugEnabled()) log.debug("No realm defined!");
          return null;
        }
        if (realm == null)
        {
          if (log.isDebugEnabled()) log.debug("No realm defined!");
          return null;
        }

        if(p.areGroupsInRealm())
        {
          log.debug("groups are given by a realm");
          principal = realm.authenticate(clientName, clientName);
          if (principal == null) log.debug("principa null with " + clientName);
        }
        if (p.isWithCommonRole())
        {
          roles = new ArrayList<String>();
          roles.add(p.getCommonRole());
        }
        if (p.areGroupsInAd())
        {
          boolean[] areGroupsInAccessToken = sspauth.areRolesInAccessToken();
          if (roles == null) roles = new ArrayList<String>();
          
          String[] groupes = trd.getGroups();
          for (int k = 0; k <groupes.length ; k++)
          {
            if ( ! areGroupsInAccessToken[k] ) continue;
            roles.add(groupes[k]);
          }
          log.debug("The roles are Windows groups");
        }//areGroupsInAd
        
        if (roles != null)
        {
          log.debug("There are some roles not given by a non windows realm");
          if (principal != null)
          {
            if (principal instanceof GenericPrincipal)
            {
              String[] sroles = ((GenericPrincipal)principal).getRoles();
              for( String role : sroles)
              {
                roles.add(role);
              }
            }
            else
            {
              log.error("principal is not a GenericPrincipal!!!!, so we cannot add the common role and the windows groups to the principal");
            }
          }//roles != null
          
          principal = new GenericPrincipal( realm,
                         clientName,
                         clientName,
                         roles);        
        }
        return principal;
   
    }

    /**
    * Called by authenticate when the client is already authenticated.
    * With the verb POST, IE send the header Authorization. We must authenticate again, else 
    * the browser could not send (NTLM) the request.
    */ 
    private boolean mustAuthenticate(Request request)
    {
      // when here, principal is not null.
      if (request.getHeader("Authorization") == null)
      {
        if (log.isDebugEnabled()) log.debug("HeaderAuthorization is not present");

        return false;
      }
      if ( request.getAttribute(ALREADYCALLED_KEY)  != null )
      //the method authenticate is called two times by tomcat during a request
      {
        if (log.isDebugEnabled()) log.debug("It is not the first call of the method authenticate by tomcat");
        return false;
      }
      else
      {
        if (log.isDebugEnabled()) log.debug("It is the first call of the method authenticate by tomcat");      
      }
      return true;
    }
    
    private AuthenticationContext getAuthenticationContext( Context ctx )
    {
      StandardContext std = (StandardContext)ctx;
      ServletContext srvctx = std.getServletContext();
      Parameters p = null;
      
      //returns the singleton if it is in the context,
      AuthenticationContext ac = (AuthenticationContext)srvctx.getAttribute(CONTEXT_KEY);
      if (ac != null) return ac;
      
      //else create the singleton Parameters and then create the instance of AuthenticationContext
      p = new Parameters( srvctx );     
      ac = new AuthenticationContext(p);

      if ( ! p.areGroupsInAd() )
      {
        srvctx.setAttribute(CONTEXT_KEY, ac);
      }

      return ac;
    }
    
	   /**
     * Get an instance of TranslationRolesIntoSids from AuthenticationContext.
     * If this instance is too old, another instance will be created.
     */
    private TranslationRolesIntoSids getTranslationRolesIntoSids( Context ctx, AuthenticationContext ac )
    {
      Parameters p = null;

      TranslationRolesIntoSids trn = ac.getTranslationRolesIntoSids();
      
      if (trn != null)
      {
        p = ac.getParameters();
        int  lifetimeTranslationMs = p.getTimeLifeMappingRolesIntoSids();
        if ( lifetimeTranslationMs < 0 ) // infinite
        {
          if (log.isDebugEnabled()) log.debug("get the instance of the current TranslationRolesIntoSids from the Context");
          return trn;
        }
        
        Date now = new Date();
        Date creationDate = trn.getDate();
        if ( now.getTime() - creationDate.getTime() < lifetimeTranslationMs )
        {        
          if (log.isDebugEnabled()) log.debug("get the instance of the current TranslationRolesIntoSids from the Context");
          return trn;
        }
        else
        {
          if (log.isDebugEnabled()) log.debug("life time of TranslationRolesIntoSids > Max lifetimeTranslationMs");
          //A new instance of TranslationRolesIntoSids is required
        }
      }
      
      trn = createTranslationNamesToSids( ctx, ac);
      return trn;
    }
    
    /**
    * Create a new instance of TranslationRolesIntoSids, call translateRolesIntoSids.
    * Put this instance in AuthenticationContext and put the singleton AuthenticationContext 
    * in the context
    */ 
    private TranslationRolesIntoSids createTranslationNamesToSids( Context ctx, AuthenticationContext ac  )
    {
      StandardContext std = (StandardContext)ctx;
      ServletContext srvctx = std.getServletContext();

      TranslationRolesIntoSids trd = new TranslationRolesIntoSids( ac );
      if (translateRolesIntoSids( trd, ctx) ) 
      {
        ac.setTranslationRolesIntoSids( trd );
        srvctx.setAttribute(CONTEXT_KEY, ac);
        return trd;
      }
      else return null;
    }
    

     /**
     * get the names of groups via <c>context.findSecurityRoles()</c><br>
     * Then try to translate the names into sids with an instance of the class TranslationRolesIntoSids.<br>
     */
    private boolean translateRolesIntoSids( TranslationRolesIntoSids trd, Context ctx)
    {
       String[] groupes = ctx.findSecurityRoles();
       
      if (log.isDebugEnabled())
      {
        log.debug("Count of groups defined in the context : " + groupes.length); 
        for (int ind = 0; ind < groupes.length ; ind++){
          log.debug(groupes[ind]);
        }
      }

      try
      {
        int nbnulls = 0;
        int sidsCount = trd.getSidsCount(groupes);
        if (log.isDebugEnabled())
        {
          log.debug("roles have been translated into sids");
          log.debug("Count of sids defined in the context : " + sidsCount); 

        }
      }
      catch(SSPException e)
      {
        log.fatal("SSPexception in getSids  : The mapping from the roles to the Active Directory's groups cannot be build" + e.getMessage());
        //response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE );
        return false;
      }
      return true;		

    }
 
     /** 
     * Search an instance of SSPAuthentication from the session, else create a new instance 
     * With NTLM, more than one exchange is required.
     */
    private SSPAuthentication 
      getSSPAuthentication( Request request, /* Response response,*/ AuthenticationContext ac ) 
    {
      String address = request.getRemoteAddr() + ":" + request.getRemotePort();
      SSPAuthentication SSPIauth = new SSPAuthentication( ac, address);

      Parameters p = ac.getParameters();
      WindowsContext winctx;
      HashMapWithTimeOut<String, WindowsContext> ctxHashMap = ac.getHashMapWithTimeOut();
      if ( ctxHashMap != null)    
      {
        winctx = getWindowsContextFromConnectionTcp( request, ctxHashMap);
      }
      else winctx = getWindowsContextFromSession( request);
            
      if (winctx != null)
      {
        SSPIauth.setWindowsContext(winctx);
        if (log.isDebugEnabled()) log.debug ( "Set the Windows Context" );
      }
      else if (log.isDebugEnabled()) log.debug ( "Create a new instance of SSPIAuthentification" );
      
      return SSPIauth;
    } 

    private WindowsContext getWindowsContextFromSession(Request request)
    {
      HttpSession session = request.getSession(false);
      if ( session == null ) return null;
      
      SSPAuthShadow sspshadow = (SSPAuthShadow) session.getAttribute(SSPIAUTHENTICATION_KEY);
      if (sspshadow == null) return null;
      WindowsContext winctx = sspshadow.getWindowsContext();
      return winctx;
    }
    
    private WindowsContext getWindowsContextFromConnectionTcp( Request request, HashMapWithTimeOut<String, WindowsContext> ctxHashMap)
    {
      String address =  request.getRemoteAddr() + ":" + request.getRemotePort();    
      return ctxHashMap.get(address);
    }

    /**
     * Set a  {@link SSPAuthentication} into the current session.
     */
    private void setSSPAuthenticationInContainer(Request request, SSPAuthentication sspauth, AuthenticationContext ac)
    {
      WindowsContext winctx = sspauth.getWindowsContext();      
      if ( winctx != null)
      {
        HashMapWithTimeOut<String, WindowsContext> ctxHashMap = ac.getHashMapWithTimeOut();
        if ( ctxHashMap != null )
        {
          String client = request.getRemoteAddr() + ":" + request.getRemotePort();
          ctxHashMap.put(client, winctx);
        }
        else
        {
          HttpSession session = request.getSession();
          //session.setAttribute(SSPIAUTHENTICATION_KEY, winctx);
          SSPAuthShadow sspshadow = new SSPAuthShadow();
          sspshadow.setWindowsContext(winctx);
          session.setAttribute(SSPIAUTHENTICATION_KEY, sspshadow);
          if (log.isDebugEnabled()) log.debug ( "set WindowsContext in a session" );
        }
      }
      else if (log.isDebugEnabled()) log.debug ( "getWindowsContext return null" );
      return;
   }  

    /**
    * Remove the link between {@link SSPAuthentication} and the current session. 
    * Called when the authentication is finished. 
    */
    private void RemoveSSPAuthenticationFromContainer(Request request, AuthenticationContext ac)
    {
      HashMapWithTimeOut<String, WindowsContext> ctxHashMap = ac.getHashMapWithTimeOut();
      if ( ctxHashMap != null)
      {
        RemoveSSPAuthenticationFromConnectionTcp( request, ctxHashMap ); 
      }
      else RemoveSSPAuthenticationFromSession( request );
    }
    
    private void RemoveSSPAuthenticationFromSession(Request request)
    {
      HttpSession session = request.getSession(false);   
      if ( session == null ) return;
      if ( session.getAttribute(SSPIAUTHENTICATION_KEY) != null )
      {
        session.removeAttribute(SSPIAUTHENTICATION_KEY);
      }
      return;
    }

    private void RemoveSSPAuthenticationFromConnectionTcp( Request request, HashMapWithTimeOut<String, WindowsContext> ctxHashMap)
    {
      String address =  request.getRemoteAddr() + ":" + request.getRemotePort();    
      ctxHashMap.remove(address);
      return;
    }
  

  /**
   *
   * <ol>
   *   <li>Verify if the clent sent a header "Authorization".
   *       If the header starts with "Negotiate"(or NTLM), calls <c>fromAuthorizationToWwwAuthenticate<c/>   
   *   <li> If the result of the method <c>fromAuthorizationToWwwAuthenticate<c/>  is non null
   *       it will be used in the header "WWW-Authenticate" of the response.
   * </ol>
   */
  private void getHeaderAuthorizationAndSetWWW_Authenticate( HttpServletRequest request,
                            HttpServletResponse response,
                            SSPAuthentication sspauth , Parameters p) 
         throws FormatBase64Exception
  {
    
    log.debug( "authentication SPNEGO: remote address: " + request.getRemoteAddr() );
    
    String authorization = request.getHeader("Authorization");   
    
    String negotiation = null;
    if (authorization != null)
    {
      negotiation = getAuthorizationType(authorization, p);
      if (negotiation == null)
      {
        authorization = null;
      }
    }
        
    if ( authorization != null ) {
      if (log.isDebugEnabled()) log.debug( "Token received from the client: " + authorization );
    }
    
    String responseToken = fromAuthorizationToWwwAuthenticate( authorization ,sspauth, p, negotiation);

    if ( responseToken != null ) {
      response.setHeader("WWW-Authenticate", negotiation + " " + responseToken);
      if (log.isDebugEnabled()) log.debug( "Sending response token: {}" + responseToken );
      return;
    }
    if ( sspauth.isEstablished() || sspauth.isFailed()  )
    {
      if (log.isDebugEnabled()) log.debug("Authentication is ended (failed or established), with a a request without  the haeder NEGOTIATE." );
      return;        
    } 
    else 
    {
      // Start of the negotiation between the server and the client
      if (log.isDebugEnabled()) log.debug(" The client did not send Authorization header: so NEGOTIATE is sent with the code 401");
      setHeadersWWWAuthenticate(p, response);
    }
  }
  
  private void setHeadersWWWAuthenticate(Parameters p, HttpServletResponse response)
  {
     if ( p.canUserChooseBetwweenSpnegoAndNntlm())
    {
      if (log.isDebugEnabled()) log.debug(" The client can choose between Negotaite and NTLM");
      response.setHeader("WWW-Authenticate", "Negotiate");
      response.addHeader("WWW-Authenticate", "NTLM");
    }
    else
    {
      if (log.isDebugEnabled()) log.debug(" The client cannot choose between Negotiate and NTLM");
      response.setHeader("WWW-Authenticate", p.NEGOTIATE);
    }

  }
   /**
   *
   * <ol>
   *   <li>Verify if the clent sent a header "Authorization" with "Negotiate" or "NTLM"
   *       If the header starts with "Negotiate", calls <c>fromAuthorizationToWwwAuthenticate<c/>   
   *   <li> If the result of the method <c>fromAuthorizationToWwwAuthenticate<c/>  is non null
   *       it will be used in the header "WWW-Authenticate" of the response.
   * </ol>
   */

  private String getAuthorizationType(String authorization, Parameters p)
  {
    
    if (authorization != null)
    {
      if ( p.canUserChooseBetwweenSpnegoAndNntlm())
      {
        if ( authorization.startsWith("Negotiate") )
        {
          return "Negotiate";
        }
        else if ( authorization.startsWith("NTLM") )
        {
          return "NTLM";
        }
      }
      else if ( authorization.startsWith(p.NEGOTIATE) )
      {
        return p.NEGOTIATE;
      }
      return null;
    }
    return null;
  }

  /**
   * Try to decode from base 64 the string sent by the client. Then send the result to  
   * <c>fromInitSecSecurityToAcceptSecurity</c>.
   * encode into Base64 the result of this last method.
   */
  private String fromAuthorizationToWwwAuthenticate( String authorization, SSPAuthentication sspauth, Parameters p, String negotiate ) 
  throws FormatBase64Exception
  {
    if (authorization != null) // && Negotiation != null) // || authorization.startsWith(p.NEGOTIATE) ) ) 
    {
      if (log.isDebugEnabled()) log.debug("Authorization != null && Authorization starts With NEGOTIATE");
        // the prefix "Negotiate " is removed from the string
      String s = authorization.substring(negotiate.length()+1);
      
      byte[] fromInitSecSecurityBase64 = s.getBytes();
      if (log.isDebugEnabled()) 
      {
        log.debug("count of bytes received from the client: " + fromInitSecSecurityBase64.length);
      }
 
      byte[] fromInitSecSecurity = null;
      
      //Remark : fromInitSecSecurity is not null, else a FormatBase64Exception has been thrown
      fromInitSecSecurity = Base64.decodeFrom64(fromInitSecSecurityBase64);
      if (log.isDebugEnabled()) 
      {
        log.debug("Count of decoded bytes received from the client after the translation from Base 64 : " + fromInitSecSecurity.length);
      }
      
      //envoie au broyage par SSPI (acceptSecContext) des octets 
      // broy�s par l'appel SSPI du client (initSecContext)
      // puis renvoie les octets resultats de ce broyage
      byte[] fromAcceptSecSecurity = null;
      fromAcceptSecSecurity = fromInitSecSecurityToAcceptSecurity( fromInitSecSecurity , sspauth);
      
      if ( fromAcceptSecSecurity == null ){
        if (log.isDebugEnabled()) log.debug(" The array of bytes sent by SSPI == null");
        return null;
      }
      byte[] fromAcceptSecSecurityBase64 = null;
      fromAcceptSecSecurityBase64 = Base64.encodeTo64(fromAcceptSecSecurity);
      if (log.isDebugEnabled()) 
      {
        log.debug("count of bytes sent to the client: " + fromAcceptSecSecurity.length);
        log.debug("count of bytes sent (base 64) to the client : " + fromAcceptSecSecurityBase64.length); 
      }
 
      // REMARK : the return will be in the header WWW_Authenticate  
      return new String(fromAcceptSecSecurityBase64);

    } 
    return null;
    
  }
  
 
  /**
   * Receive the bytes sent by the client .(result of <c>initSecContext</c>)
   * Return the bytes return by  <c>acceptSecContext</c>
   */
  private byte[] fromInitSecSecurityToAcceptSecurity(byte[] fromInitSecSecurity, SSPAuthentication sspauth)
  {

    // preconditions
    if (fromInitSecSecurity == null) return null;
    
    byte[] fromAcceptSecSecurity = null;    
    try
    {
      if (log.isDebugEnabled()) log.debug("before sspauth.acceptSecContext");
      fromAcceptSecSecurity = sspauth.acceptSecContext(fromInitSecSecurity);

      if (log.isDebugEnabled()){
        if ( fromAcceptSecSecurity != null) log.debug("count of bytes received from acceptSecContext : " + fromAcceptSecSecurity.length);
      }
      
      if (sspauth.isEstablished())
      {
        if (log.isDebugEnabled()){
          log.debug("Authenticated user : " + sspauth.getUserName());
          log.debug("Security Service Provider Interface used : " + sspauth.getSSP());
        }	
      } 
      else
      // NTLM
      {
        if (log.isDebugEnabled()) log.debug("Another cycle initSecContext/acceptSecContext is required");
      }
    }
    catch (SSPException e){
      //Authentication failed
      fromAcceptSecSecurity = null;
      if (log.isDebugEnabled()) log.debug("SSPIAuthentication : " + e.getMessage() );
    }

    if (fromAcceptSecSecurity == null)
    {
      if (log.isDebugEnabled()) log.debug("array of bytes sent by acceptSecContext (SSPI server) is null");
		//return null;
    }
    return fromAcceptSecSecurity;

  }//fromInitSecSecurityToAcceptSecurity
  
  private boolean isNtlmAuthentication( Parameters p, Request request)
  {
    String authorization = request.getHeader("Authorization");
    if (authorization != null && authorization.startsWith(p.NEGOTIATE + " TlRMTVNTUA")) return true;
    return false; 
  }
  
  protected String getAuthMethod()
  {
    return "DIGEST";
  }

  
  protected Principal doLogin(Request request, String username,String password) 
    throws ServletException 
  {
    if ( (username == null) || (username.length()  == 0) || (password == null) || (password.length() == 0)  || (request == null) )
    {
      throw new ServletException("username or password or request is unknown"); 
    }
    //Principal p = context.getRealm().authenticate(username, password);

    if (log.isDebugEnabled()) log.debug("Authentication via doLogin");

    TranslationRolesIntoSids trd = null;
    AuthenticationContext ac = null;
    
    Context ctx = request.getContext();
    ac  = getAuthenticationContext(ctx);
    if (ac == null)
    {
      if (log.isDebugEnabled()) log.debug("Authentication Context cannot be created");
      throw new ServletException("Authentication Context cannot be created");
    }

    Parameters p = ac.getParameters();

    Principal principal = null;
    Realm realm;
    try
    {
      realm = context.getRealm();
    }
    catch(Exception e)
    {
      if (log.isDebugEnabled()) log.debug("No realm defined!");
      throw new ServletException("no realm defined.");
    }
    
    if (p.isLoginWithoutAD())
    {
      //
      principal = realm.authenticate(username, password);
      if (principal != null)
      {
        HttpSession session = request.getSession(p.mustCreateSession());
        if (log.isDebugEnabled()) log.debug("User authenticated with login/password");
        if (session != null) session.setAttribute(LOGIN_KEY, "1");
      }
      return principal;
    }
    
    if (p.areGroupsInAd())
    {
      trd  = getTranslationRolesIntoSids(ctx, ac);
      if (trd == null)
      {
        if (log.isDebugEnabled()) log.debug("No translation of the roles into SIDs");
        throw new ServletException("No translation of the roles into SIDs");
      }
    }
    
    String clientName= null;
    SSPAuthentication sspauth = new SSPAuthentication(ac);
    try
    {
      sspauth.login( username, password);
    }
    catch( SSPException e)
    {
      throw new ServletException(e.getMessage() );
    }
    if (sspauth.isEstablished())
    {
      if (log.isDebugEnabled()) log.debug("Authentication is established");
      //try/catch required by java. 
      // Exception is not possible cf( the precondition isEstablished() in SSPAuthentication)
      try
      {         
        clientName = sspauth.getUserName();
      }
      catch (SSPException e) { log.fatal("This Exception could not exist: " + e.getMessage()); }
    }
    else
    {
      if (log.isDebugEnabled()) log.debug("Authentication is failed"); 
            throw new ServletException(sm.getString("authenticator.loginFail"));

    }
    if (clientName != null)
    {
      if (p.isUserNameWithoutDomainName())
      {
        //SSPI send a name like DomaiName\ClientName
        //We want to have only ClientName
        int position = clientName.indexOf('\\');
        String clientTemp = clientName;
        if (clientTemp.length() > position){
          clientTemp = clientTemp.substring(position+1);
        }
        if (log.isDebugEnabled()){
          if (log.isDebugEnabled()) log.debug("clientName :" + clientTemp);
        }
        clientName = clientTemp;
      }

      if (p.areGroupsInAd())
      {  
        boolean[] areGroupsInAccessToken = sspauth.areRolesInAccessToken();
        ArrayList<String> roles = new ArrayList<String>();
        
        String[] groupes = trd.getGroups();
        for (int k = 0; k <groupes.length ; k++)
        {
          if ( ! areGroupsInAccessToken[k] ) continue;
          roles.add(groupes[k]);
        }
        // Creation of the principal
          principal = new GenericPrincipal( realm,
                       clientName,
                       clientName,
                       roles);       
      }
      else
      {
        //authentification aupr�s du realm configur� pour r�cup�rer un principal avec les roles.
        principal = realm.authenticate(clientName, clientName);
 
      }
      if (principal != null)
      {
        HttpSession session = request.getSession(p.mustCreateSession());
        if (log.isDebugEnabled()) log.debug("the client is now authenticated");
        if (session != null)session.setAttribute(LOGIN_KEY, "1");
        return principal;
      }
      else
      {
        //We cannot be here, if a realm is defined
        if (log.isDebugEnabled())
        {
            log.debug("User authenticated but we cannot create a new GenericPrincipal.");
            throw new ServletException(sm.getString("authenticator.loginFail"));
        }
      }
    } //if (clientName != null)
    if (log.isDebugEnabled()) log.debug("clientName == null && authenticated!!! curious..."); 
            throw new ServletException(sm.getString("authenticator.loginFail"));
  }
    public void logout(Request request)
    throws ServletException
    {
      if (request == null) throw new ServletException(" request is null");
      super.logout(request);
      HttpSession session = request.getSession(false);
      if (session != null) session.invalidate();
    }
   // To add in AUthenticator with tc6 or jboss 6
    public void login(String username, String password, Request request)
    throws ServletException {
        Principal principal = doLogin(request, username, password);
        register(request, request.getResponse(), principal,// request.getResponse()? httpServletRequest.getResponse()
                    "DIGEST",
                    username, password);  
    }
}
